import os
import sys
import re
from datetime import datetime, date
from pathlib import Path
from pyecharts import options as opts
from pyecharts.charts import Bar, Pie
from pyecharts.commons.utils import JsCode
from pyecharts.globals import ThemeType


class LogAnalyzer:
    def __init__(self, log_path, start_str, end_str):
        self.log_path = log_path
        self.start_str = start_str.strip()
        self.end_str = end_str.strip()
        self.log_entries = []
        self.log_dates = []
        self.valid_log_types = set()
        self.result = {}
        self.error_types = {}
        self.final_start = None
        self.final_end = None
        self.analyzed_files = []
        self.error_log_details = []  # 存储错误日志的完整详情

        # 适配默认日志格式的正则表达式
        # 匹配格式：YYYY-MM-DD HH:MM:SS [ thread_id ] [ LEVEL ] ...
        # 提取日期(YYYY-MM-DD)和日志级别(LEVEL)
        self.log_pattern = re.compile(
            r'^(\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2} \[ .*? \] \[ (\w+) \]'
        )

        self._precheck_time_format()

    def _precheck_time_format(self):
        time_formats = [r'^\d{4}-\d{2}-\d{2}$', r'^\d{2}-\d{2}$']
        for param in [self.start_str, self.end_str]:
            if not any(re.match(fmt, param) for fmt in time_formats):
                raise ValueError(
                    f"时间格式错误: {param}\n"
                    "请用 xxxx-xx-xx（完整日期）或 xx-xx（月-日）"
                )

    def collect_log_files(self):
        log_files = []
        path_obj = Path(self.log_path).resolve()

        if not path_obj.exists():
            raise FileNotFoundError(f"路径不存在: {path_obj}")
        if not os.access(path_obj, os.R_OK):
            raise PermissionError(f"无权限访问: {path_obj}")

        if path_obj.is_file():
            if os.access(path_obj, os.R_OK):
                log_files.append(path_obj)
            else:
                raise PermissionError(f"无权限读文件: {path_obj}")
        elif path_obj.is_dir():
            # 只遍历一级目录
            for item in path_obj.iterdir():
                if item.is_file() and not item.name.startswith('.'):
                    if os.access(item, os.R_OK):
                        log_files.append(item)
                    else:
                        print(f"警告: 无权限读 {item}，已跳过", file=sys.stderr)
        else:
            raise ValueError(f"无效路径（非文件/目录）: {path_obj}")

        if not log_files:
            dir_content = [f.name for f in path_obj.iterdir() if f.is_file()]
            raise ValueError(f"无有效日志文件: {path_obj}（目录下文件：{dir_content}）")
        return log_files

    def parse_log_file(self, file_path):
        try:
            encodings = ['utf-8', 'gbk', 'latin-1']
            content = None
            for encoding in encodings:
                try:
                    with open(file_path, 'r', encoding=encoding) as f:
                        content = f.readlines()
                    break
                except UnicodeDecodeError:
                    continue
            if content is None:
                print(f"警告: 无法解析 {file_path}，已跳过", file=sys.stderr)
                return

            valid_count = 0
            for line_num, line in enumerate(content, 1):
                original_line = line
                line = line.strip()
                if not line:
                    continue

                match = self.log_pattern.match(line)
                if match:
                    date_str, log_type = match.groups()
                    try:
                        log_date = datetime.strptime(date_str, '%Y-%m-%d').date()
                        self.log_entries.append((log_date, log_type))
                        self.log_dates.append(log_date)
                        self.valid_log_types.add(log_type)
                        valid_count += 1

                        # 收集ERROR级别日志详情
                        if log_type.upper() == "ERROR" or log_type.upper().startswith("ERROR_"):
                            self.error_log_details.append({
                                "file_path": str(file_path),
                                "line_num": line_num,
                                "log_date": log_date,
                                "log_type": log_type,
                                "full_content": original_line.rstrip()
                            })
                    except ValueError:
                        print(f"警告: {file_path} 第{line_num}行日期错误: {date_str}", file=sys.stderr)

            self.analyzed_files.append({
                "path": str(file_path),
                "valid_count": valid_count
            })
            print(f"解析完成: {file_path}（有效日志: {valid_count}条）")

        except Exception as e:
            print(f"错误: 解析 {file_path} 失败: {str(e)}", file=sys.stderr)

    def parse_time_param(self, time_str, log_years):
        if re.match(r'^\d{4}-\d{2}-\d{2}$', time_str):
            try:
                return datetime.strptime(time_str, '%Y-%m-%d').date()
            except ValueError:
                raise ValueError(f"无效日期: {time_str}（如2月30日）")

        if re.match(r'^\d{2}-\d{2}$', time_str):
            try:
                month, day = map(int, time_str.split('-'))
                sorted_years = sorted(set(d.year for d in self.log_dates)) if self.log_dates else []
            
                if sorted_years:
                    for year in sorted_years:
                        try:
                            return date(year, month, day)
                        except ValueError:
                            continue
                    raise ValueError(f"月-日 {time_str} 在日志年份 {sorted_years} 中无效")
                else:
                    return date(datetime.now().year, month, day)
            except ValueError as e:
                raise ValueError(f"无效月-日: {time_str}（{str(e)}）")

        raise ValueError(f"未识别格式: {time_str}")

    def determine_time_range(self):
        if not self.log_dates:
            current_year = datetime.now().year
            start_date = self.parse_time_param(self.start_str, [current_year])
            end_date = self.parse_time_param(self.end_str, [current_year])
        else:
            log_years = sorted(set(d.year for d in self.log_dates))
            start_date = self.parse_time_param(self.start_str, log_years)
            end_date = self.parse_time_param(self.end_str, log_years)

        if start_date > end_date:
            start_date, end_date = end_date, start_date
            print(f"警告: 自动交换时间范围为 {start_date} 至 {end_date}", file=sys.stderr)

        self.final_start = start_date
        self.final_end = end_date

        print(f"最终分析时间范围: {self.final_start} 至 {self.final_end}")
        return self.final_start, self.final_end

    def count_log_types(self, start_date, end_date):
        self.result = {log_type: 0 for log_type in self.valid_log_types}

        for log_date, log_type in self.log_entries:
            if start_date <= log_date <= end_date:
                self.result[log_type] += 1

        self.result = {k: v for k, v in self.result.items() if v > 0}
        self.error_types = {
            k: v for k, v in self.result.items()
            if k.upper() == "ERROR" or k.upper().startswith("ERROR_")
        }

        if not self.result:
            print("警告: 时间范围内无有效日志", file=sys.stderr)

    def generate_visualization(self):
        total_logs = sum(self.result.values()) if self.result else 0
        total_types = len(self.result)
        error_total = sum(self.error_types.values()) if self.error_types else 0
        error_ratio = (error_total / total_logs * 100) if total_logs > 0 else 0

        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        html_filename = f"log_analysis_report_{timestamp}.html"

        style = """
        <style>
            :root {
                --primary: #2c3e50;
                --error: #e74c3c;
                --warning: #f39c12;
                --info: #3498db;
                --success: #2ecc71;
                --light: #ecf0f1;
            }
            body { 
                font-family: 'Segoe UI', Arial, sans-serif; 
                margin: 0; 
                padding: 20px; 
                background-color: #f9f9f9;
                color: var(--primary);
            }
            .report-container {
                max-width: 1200px;
                margin: 0 auto;
                background: white;
                padding: 30px;
                border-radius: 10px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            }
            .header {
                border-bottom: 3px solid var(--light);
                padding-bottom: 20px;
                margin-bottom: 30px;
            }
            .stats-grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
                gap: 20px;
                margin: 20px 0 30px 0;
            }
            .stat-card {
                background: white;
                padding: 15px;
                border-radius: 8px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.08);
                border-left: 4px solid;
            }
            .stat-card.total { border-left-color: var(--info); }
            .stat-card.types { border-left-color: var(--success); }
            .stat-card.errors { border-left-color: var(--error); }
            .stat-card.ratio { border-left-color: var(--warning); }
            .section { margin-bottom: 40px; }
            .section h2 {
                color: var(--primary);
                border-bottom: 2px solid var(--light);
                padding-bottom: 10px;
            }
            .error-highlight {
                background-color: rgba(231, 76, 60, 0.1);
                border-radius: 8px;
                padding: 20px;
                margin-bottom: 30px;
            }
            .error-highlight h2 { color: var(--error); }
            .chart-container { width: 100%; height: 500px; margin: 20px 0; }
            .file-list {
                max-height: 300px;
                overflow-y: auto;
                border: 1px solid var(--light);
                border-radius: 8px;
                padding: 15px;
            }
            .file-item {
                padding: 8px 0;
                border-bottom: 1px dashed var(--light);
            }
            .file-item .count { color: var(--info); font-weight: bold; }
            .legend {
                display: flex;
                flex-wrap: wrap;
                gap: 15px;
                margin: 10px 0 20px 0;
            }
            .legend-item { display: flex; align-items: center; }
            .legend-color {
                width: 15px;
                height: 15px;
                border-radius: 3px;
                margin-right: 5px;
            }
            .error-detail-table {
                width: 100%;
                border-collapse: collapse;
                font-size: 14px;
            }
            .error-detail-table th {
                background-color: var(--error);
                color: white;
                padding: 12px;
                text-align: left;
                border: 1px solid #ddd;
            }
            .error-detail-table td {
                padding: 10px;
                border: 1px solid #ddd;
                vertical-align: top;
            }
            .error-detail-table tr:nth-child(even) {
                background-color: #fdf2f2;
            }
            .error-detail-container {
                max-height: 400px;
                overflow-y: auto;
                margin-top: 15px;
                border: 1px solid #f1c4c4;
                border-radius: 8px;
            }
            .file-path { color: var(--info); font-weight: 500; }
            .line-num { color: var(--warning); font-weight: bold; }
            .log-content {
                white-space: pre-wrap;
                word-break: break-all;
                background-color: #fff8f8;
                padding: 8px;
                border-radius: 4px;
                border-left: 3px solid var(--error);
            }
        </style>
        """

        html_content = f"""
        <!DOCTYPE html>
        <html lang='zh-CN'>
        <head>
            <meta charset='UTF-8'>
            <meta name='viewport' content='width=device-width, initial-scale=1.0'>
            <title>日志分析报告 - {timestamp}</title>
            {style}
        </head>
        <body>
            <div class='report-container'>
                <div class='header'>
                    <h1>日志分析报告</h1>
                    <p><strong>生成时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
                    <p><strong>分析路径:</strong> {self.log_path}</p>
                    <p><strong>时间范围:</strong> {self.final_start} 至 {self.final_end}</p>
                </div>

                <div class='stats-grid'>
                    <div class='stat-card total'>
                        <h3>总日志记录数</h3>
                        <p class='value'>{total_logs}</p>
                    </div>
                    <div class='stat-card types'>
                        <h3>日志类型总数</h3>
                        <p class='value'>{total_types}</p>
                    </div>
                    <div class='stat-card errors'>
                        <h3>ERROR类型总数</h3>
                        <p class='value'>{len(self.error_types)}</p>
                    </div>
                    <div class='stat-card ratio'>
                        <h3>ERROR占比</h3>
                        <p class='value'>{error_ratio:.1f}%</p>
                    </div>
                </div>

                <div class='error-highlight section'>
                    <h2>ERROR类型详细统计</h2>
                    {self._generate_error_details()}
                </div>

                <div class='section'>
                    <h2>所有日志类型分布</h2>
                    <div class='legend'>
                        <div class='legend-item'>
                            <div class='legend-color' style='background-color: var(--error);'></div>
                            <span>ERROR类型</span>
                        </div>
                        <div class='legend-item'>
                            <div class='legend-color' style='background-color: var(--info);'></div>
                            <span>其他类型</span>
                        </div>
                    </div>
                    {self._generate_all_types_charts()}
                </div>

                <div class='section'>
                    <h2>分析的日志文件</h2>
                    <div class='file-list'>
                        {self._generate_file_list()}
                    </div>
                </div>
            </div>
        </body>
        </html>
        """

        with open(html_filename, "w", encoding="utf-8") as f:
            f.write(html_content.strip())

        print(f"可视化报告已生成: {os.path.abspath(html_filename)}")

    def _generate_error_details(self):
        if not self.error_types:
            return "<p>未发现ERROR类型日志</p>"

        sorted_errors = sorted(self.error_types.items(), key=lambda x: x[1], reverse=True)
        total_logs = sum(self.result.values())

        error_table = "<table style='width:100%; border-collapse: collapse; margin-top: 20px;'>"
        error_table += """
        <tr style='background-color: #f1f1f1;'>
            <th style='padding: 12px; text-align: left; border-bottom: 2px solid var(--error);'>ERROR类型</th>
            <th style='padding: 12px; text-align: right; border-bottom: 2px solid var(--error);'>出现次数</th>
            <th style='padding: 12px; text-align: right; border-bottom: 2px solid var(--error);'>占比</th>
        </tr>
        """
        for log_type, count in sorted_errors:
            ratio = (count / total_logs * 100) if total_logs else 0
            error_table += f"""
            <tr>
                <td style='padding: 10px; border-bottom: 1px solid #eee; font-weight: bold; color: var(--error);'>{log_type}</td>
                <td style='padding: 10px; text-align: right; border-bottom: 1px solid #eee;'>{count}</td>
                <td style='padding: 10px; text-align: right; border-bottom: 1px solid #eee;'>{ratio:.1f}%</td>
            </tr>
            """
        error_table += "</table>"

        error_pie = (
            Pie(init_opts=opts.InitOpts(theme=ThemeType.ESSOS))
            .add(
                "ERROR类型分布",
                sorted_errors,
                radius=["40%", "70%"],
                label_opts=opts.LabelOpts(
                    formatter=JsCode("function(p){return p.name + ': ' + p.value + ' (' + p.percent.toFixed(1) + '%)';}")
                ),
            )
            .set_global_opts(title_opts=opts.TitleOpts(title="ERROR类型占比", pos_top="20px"))
            .set_series_opts(
                itemstyle_opts=opts.ItemStyleOpts(
                    color=JsCode("""function(params) {
                        const colors = ['#e74c3c', '#c0392b', '#e67e22', '#d35400'];
                        return colors[params.dataIndex % colors.length];
                    }""")
                )
            )
        )

        filtered_error_details = sorted(
            [log for log in self.error_log_details if self.final_start <= log["log_date"] <= self.final_end],
            key=lambda x: x["log_date"]
        )
        
        def escape_html(text):
            return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        
        detail_table = f"""
        <p><strong>当前时间范围错误日志详情（共{len(filtered_error_details)}条，按时间从旧到新排序）：</strong></p>
        <div class='error-detail-container'>
            <table class='error-detail-table'>
                <tr>
                    <th>日志文件</th>
                    <th>行号</th>
                    <th>日期</th>
                    <th>ERROR类型</th>
                    <th>完整内容</th>
                </tr>
        """
        if not filtered_error_details:
            detail_table += """
                <tr>
                    <td colspan='5' style='text-align: center; color: #666; padding: 20px;'>
                        当前时间范围内无具体ERROR日志记录
                    </td>
                </tr>
            """
        else:
            for log in filtered_error_details:
                short_filename = os.path.basename(log["file_path"])
                detail_table += f"""
                <tr>
                    <td class='file-path'>{short_filename}</td>
                    <td class='line-num'>{log['line_num']}</td>
                    <td>{log['log_date']}</td>
                    <td>{log['log_type']}</td>
                    <td><div class='log-content'>{escape_html(log['full_content'])}</div></td>
                </tr>
                """
        detail_table += """
            </table>
        </div>
        """

        return f"""
        <p>共发现 {len(self.error_types)} 种ERROR类型，总计 {sum(self.error_types.values())} 条错误日志</p>
        {detail_table}
        <p style='margin-top: 20px;'><strong>ERROR类型汇总统计：</strong></p>
        {error_table}
        <div class='chart-container' style='height: 400px;'>{error_pie.render_embed()}</div>
        """

    def _generate_all_types_charts(self):
        if not self.result:
            return "<p>没有可展示的日志类型数据</p>"

        sorted_items = sorted(self.result.items(), key=lambda x: x[1], reverse=True)
        top_n = 50
        top_items = sorted_items[:top_n]
        other_count = sum(v for k, v in sorted_items[top_n:]) if len(sorted_items) > top_n else 0
        if other_count > 0:
            top_items.append(("其他类型", other_count))

        item_colors = []
        for name, _ in top_items:
            if name.upper() == "ERROR" or name.upper().startswith("ERROR_"):
                item_colors.append("#e74c3c")
            elif name == "其他类型":
                item_colors.append("#95a5a6")
            else:
                item_colors.append("#3498db")

        bar = (
            Bar(init_opts=opts.InitOpts(theme=ThemeType.LIGHT))
            .add_xaxis([item[0] for item in top_items])
            .add_yaxis(
                "出现次数",
                [item[1] for item in top_items],
                itemstyle_opts=opts.ItemStyleOpts(color=JsCode(f"""
                    function(params) {{ return ['{ "','".join(item_colors) }'][params.dataIndex];
                }}"""))
            )
            .set_global_opts(
                title_opts=opts.TitleOpts(title=f"日志类型出现次数（前{top_n}种）"),
                xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=-45)),
                datazoom_opts=[opts.DataZoomOpts()],
            )
        )

        pie = (
            Pie(init_opts=opts.InitOpts(theme=ThemeType.VINTAGE))
            .add(
                "日志类型占比",
                top_items,
                radius=["40%", "70%"],
                label_opts=opts.LabelOpts(
                    formatter=JsCode("function(p){return p.name + ': ' + p.value + ' (' + p.percent.toFixed(1) + '%)';}")
                ),
            )
            .set_global_opts(legend_opts=opts.LegendOpts(orient="vertical", pos_left="left"))
            .set_series_opts(itemstyle_opts=opts.ItemStyleOpts(color=item_colors))
        )

        return f"""
        <div class='chart-container'>{bar.render_embed()}</div>
        <div class='chart-container'>{pie.render_embed()}</div>
        """

    def _generate_file_list(self):
        if not self.analyzed_files:
            return "<p>没有分析过的文件</p>"

        file_html = ""
        for file in self.analyzed_files:
            file_html += f"""
            <div class='file-item'>
                <span>{file['path']}</span>
                <span class='count'>有效日志: {file['valid_count']}条</span>
            </div>
            """
        return file_html

    def run(self):
        try:
            print("===== 开始日志分析 =====")
            log_files = self.collect_log_files()
            print(f"步骤1/4: 发现 {len(log_files)} 个可分析文件")

            print("步骤2/4: 开始解析日志文件...")
            for file in log_files:
                self.parse_log_file(file)
            print(f"步骤2/4: 解析完成（共 {len(self.log_entries)} 条有效记录）")

            print("步骤3/4: 计算时间范围...")
            self.determine_time_range()

            print("步骤4/4: 统计日志类型...")
            self.count_log_types(self.final_start, self.final_end)

            print("生成可视化报告...")
            self.generate_visualization()
            print("===== 分析完成 =====")

        except Exception as e:
            print(f"\n===== 分析失败: {str(e)} =====", file=sys.stderr)
            sys.exit(1)


def main():
    if len(sys.argv) != 4:
        script_name = os.path.basename(sys.argv[0])
        print(f"用法: python {script_name} <日志路径> <开始时间> <结束时间>", file=sys.stderr)
        print("示例:")
        print(f"  python {script_name} ./app.log 2023-01-01 2023-12-31")
        print(f"  python {script_name} ./logs 01-01 12-31")
        sys.exit(1)

    analyzer = LogAnalyzer(
        log_path=sys.argv[1],
        start_str=sys.argv[2],
        end_str=sys.argv[3]
    )
    analyzer.run()


if __name__ == "__main__":
    main()